use crate::lang::command::OutputType::Known;
use crate::lang::data::r#struct::Struct;
use crate::lang::data::table::Row;
use crate::lang::errors::{CrushResult, command_error};
use crate::lang::ordered_string_map::OrderedStringMap;
use crate::lang::state::contexts::CommandContext;
use crate::lang::state::scope::Scope;
use crate::lang::value::Value;
use crate::lang::{data::table::ColumnType, value::ValueType};
use chrono::{DateTime, Local};
use signature::signature;
use std::convert::TryFrom;
use systemd::journal::{Journal, JournalFiles, JournalSeek};

static JOURNAL_OUTPUT_TYPE: [ColumnType; 2] = [
    ColumnType::new("time", ValueType::Time),
    ColumnType::new("data", ValueType::Struct),
];

#[signature(
    systemd.journal,
    can_block = true,
    output = Known(ValueType::table_input_stream(&JOURNAL_OUTPUT_TYPE)),
    short = "Show the systemd journal",
    example = "# Show last 10 entries for pid 123",
    example = "systemd:journal _PID=\"123\" | tail"
)]
struct JournalSignature {
    #[description("wait indefinitely for more data once the end of the journal is reached.")]
    #[default(false)]
    follow: bool,
    #[description("ignore system logs.")]
    #[default(false)]
    skip_system_files: bool,
    #[description("ignore the user specific logs of this user.")]
    #[default(false)]
    skip_user_files: bool,
    #[description("start reading at the end of the log.")]
    #[default(false)]
    runtime_only: bool,
    #[description("only show logs generated by localhost.")]
    #[default(false)]
    local_only: bool,
    #[description("only show logs emitted after the specified timestamp.")]
    seek: Option<Value>,
    #[description("filter to only show journal entries with the specified key-value pairs.")]
    #[named()]
    filters: OrderedStringMap<String>,
}

fn parse_files(cfg: &JournalSignature) -> CrushResult<JournalFiles> {
    match (!cfg.skip_system_files, !cfg.skip_user_files) {
        (true, true) => Ok(JournalFiles::All),
        (true, false) => Ok(JournalFiles::System),
        (false, true) => Ok(JournalFiles::CurrentUser),
        (false, false) => command_error("No files specified"),
    }
}

fn usec_since_epoch(tm: DateTime<Local>) -> CrushResult<u64> {
    let epoch = DateTime::from(std::time::UNIX_EPOCH);
    let duration = tm - epoch;
    Ok(u64::try_from(
        duration.num_microseconds().ok_or("Time overflow")?,
    )?)
}

fn journal(mut context: CommandContext) -> CrushResult<()> {
    let cfg: JournalSignature =
        JournalSignature::parse(context.remove_arguments(), &context.global_state.printer())?;
    let mut journal = Journal::open(parse_files(&cfg)?, cfg.runtime_only, cfg.local_only)?;

    match cfg.seek {
        Some(Value::Time(tm)) => {
            journal.seek(JournalSeek::ClockRealtime {
                usec: usec_since_epoch(tm)?,
            })?;
        }
        Some(v) => {
            return command_error(format!("Don't know how to seek to {}", v.value_type()));
        }
        None => {}
    }

    for (key, value) in &cfg.filters {
        journal.match_add(key, value.as_bytes())?;
    }

    let output = context.output.initialize(&JOURNAL_OUTPUT_TYPE)?;

    loop {
        match if cfg.follow {
            journal.await_next_entry(None)
        } else {
            journal.next_entry()
        }? {
            None => {
                if !cfg.follow {
                    break;
                }
            }
            Some(row) => {
                let data = Value::Struct(Struct::new(
                    row.iter()
                        .map(|(k, v)| (k, Value::from(v.clone())))
                        .collect(),
                    None,
                ));
                output.send(Row::new(vec![
                    Value::Time(DateTime::from(journal.timestamp()?)),
                    data,
                ]))?;
            }
        }
    }
    Ok(())
}

pub fn declare(root: &Scope) -> CrushResult<()> {
    root.create_namespace(
        "systemd",
        "Systemd commands",
        Box::new(move |systemd| {
            JournalSignature::declare(systemd)?;
            Ok(())
        }),
    )?;
    Ok(())
}
